/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

import * as path from 'path';

//import { languages, ExtensionContext, IndentAction, Position, TextDocument, Color, ColorInformation, ColorPresentation, Range, CompletionItem, CompletionItemKind, SnippetString } from 'vscode';
import { LanguageClient, ServerOptions, TransportKind } from 'vscode-languageclient';
import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared';
import { activateTagClosing } from './tagClosing';
import * as vsclient from "vscode-languageclient";

import * as vsproto from 'vscode-languageserver-protocol';
import { RequestType, TextDocumentPositionParams, DocumentSymbolRequest, DocumentRangeFormattingRequest, SignatureHelpRequest, ReferencesRequest, DefinitionRequest, DocumentHighlightRequest, HoverRequest, CompletionResolveRequest, CompletionRequest, DocumentLinkRequest } from "vscode-languageserver-protocol";
import { getLanguageModes } from './modes/languageModes';
import { format } from "./modes/formatting";
import { pushAll } from "./utils/arrays";
import { Position } from "vscode-languageserver-types";
import { getDocumentContext } from './utils/documentContext';

namespace TagCloseRequest {
    export const type: RequestType<TextDocumentPositionParams, string, any, any> = new RequestType('html/tag');
}
function formatError(message: string, err: any): string {
    if (err instanceof Error) {
        let error = <Error>err;
        return `${message}: ${error.message}\n${error.stack}`;
    } else if (typeof err === 'string') {
        return `${message}: ${err}`;
    } else if (err) {
        return `${message}: ${err.toString()}`;
    }
    return message;
}

function runSafe<T>(func: () => Thenable<T> | T, errorVal: T, errorMessage: string, detail: any): Thenable<T> | T {
    try {
        let t = func();
        if (t instanceof Promise) {
            return t.then((v) => {
                detail.resolver(v);
                return v;
            }, e => {
                detail.rejecter(formatError(errorMessage, e));
                console.error(formatError(errorMessage, e));
                return errorVal;
            });
        }
        detail.resolver(t);
        return t;
    } catch (e) {
        console.error(formatError(errorMessage, e));
        return errorVal;
    }
}

var vscode: any;
export function activate(context: any) {
    vscode = (<any>window)["getVsCode"]("html");
    let toDispose = context.subscriptions;

    // The server is implemented in node
    let serverModule = context.asAbsolutePath(path.join('server', 'out', 'htmlServerMain.js'));
    // The debug options for the server
    let debugOptions = { execArgv: ['--nolazy', '--inspect=6045'] };

    // If the extension is launch in debug mode the debug server options are use
    // Otherwise the run options are used
    let serverOptions: ServerOptions = {
        run: { module: serverModule, transport: TransportKind.ipc },
        debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
    };

    let documentSelector = ['html', 'handlebars', 'razor'];
    let embeddedLanguages = { css: true, javascript: true };

    // Options to control the language client
    let clientOptions = {
        documentSelector,
        synchronize: {
            configurationSection: ['html', 'css', 'javascript'], // the settings to synchronize
        },
        initializationOptions: {
            embeddedLanguages
        },
        vscode: vscode
    };

    // Create the language client and start the client.
    let workspaceFolders: any[];
    var languageModes: any;
    let clientSnippetSupport;
    let scopedSettingsSupport: boolean;
    var globalSettings: any = {};
    var documents = new Map<any, any>();
    vscode.workspace.textDocuments.forEach((doc: any) => {
        documents.set(doc.uri.toString(), doc);
    });
    function getDocumentSettings(textDocument: any, needsDocumentSettings: () => boolean): Thenable<any | undefined> {
        if (scopedSettingsSupport && needsDocumentSettings()) {
            let promise = documents.get(textDocument.uri.toString());
            if (!promise) {
                let scopeUri = textDocument.uri;
                let configRequestParam: any = { items: [{ scopeUri, section: 'css' }, { scopeUri, section: 'html' }, { scopeUri, section: 'javascript' }] };
                var uuid = (<any>vsclient).UUID.generateUuid();
                promise = new Promise<any>((resolve, reject) => {
                    var ev = new CustomEvent("html_request_" + (<any>vsproto).ConfigurationRequest.type.method, {
                        detail: {
                            uuid: uuid,
                            params: configRequestParam,
                            resolver: resolve,
                            rejecter: reject
                        }
                    });
                    window.dispatchEvent(ev);
                }).then(s => ({ css: s[0], html: s[1], javascript: s[2] }));
                documents.set(textDocument.uri.toString(), promise);
            }
            return promise;
        }
        return Promise.resolve(void 0);
    }
    window.addEventListener("html_request_initialize", (ev: any) => {
        var detail = ev.detail.params;
        let initializationOptions = detail.initializationOptions;

        workspaceFolders = detail.workspaceFolders;
        if (!Array.isArray(workspaceFolders)) {
            workspaceFolders = [];
            if (detail.rootPath) {
                workspaceFolders.push({ name: '', uri: vscode.uri.file(detail.rootPath).toString() });
            }
        }

        languageModes = getLanguageModes(initializationOptions ? initializationOptions.embeddedLanguages : { css: true, javascript: true });
        vscode.workspace.onDidCloseTextDocument((doc: any) => {
            languageModes.onDocumentRemoved(doc);
            documents.delete(doc.uri.toString());
        });
        vscode.workspace.onDidOpenTextDocument((doc: any) => {
            documents.set(doc.uri.toString(), doc);
        });
        function hasClientCapability(...keys: string[]) {
            let c = <any>detail.capabilities;
            for (let i = 0; c && i < keys.length; i++) {
                c = c[keys[i]];
            }
            return !!c;
        }

        clientSnippetSupport = hasClientCapability('textDocument', 'completion', 'completionItem', 'snippetSupport');
        //let clientDynamicRegisterSupport = hasClientCapability('workspace', 'symbol', 'dynamicRegistration');
        scopedSettingsSupport = hasClientCapability('workspace', 'configuration');
        //let workspaceFoldersSupport = hasClientCapability('workspace', 'workspaceFolders');
        let capabilities = {
            // Tell the client that the server works in FULL text document sync mode
            textDocumentSync: 1,// documents.syncKind,
            completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : undefined,
            hoverProvider: true,
            documentHighlightProvider: true,
            documentRangeFormattingProvider: false,
            documentLinkProvider: { resolveProvider: false },
            documentSymbolProvider: true,
            definitionProvider: true,
            signatureHelpProvider: { triggerCharacters: ['('] },
            referencesProvider: true,
            colorProvider: true
        };
        var cb = ev.detail["requests"].get("html_request_initialize_" + ev.detail.uuid);
        cb && cb({ capabilities });
    });
    let client = new LanguageClient('html', 'HTML Language Server', serverOptions, clientOptions);
    client.registerProposedFeatures();

    client.start();
    client.onReady().then(() => {
        // let disposable = vscode.languages.registerColorProvider(documentSelector, {
        //     provideDocumentColors(document: any): Thenable<any[]> {
        //         let params = {
        //             textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document)
        //         };
        //         return client.sendRequest((<any>vsproto).DocumentColorRequest.type, params).then(symbols => {
        //             return (<any>symbols).map((symbol: any) => {
        //                 let range = client.protocol2CodeConverter.asRange(symbol.range);
        //                 let color = new vscode.Color(symbol.color.red, symbol.color.green, symbol.color.blue, symbol.color.alpha);
        //                 return new vscode.ColorInformation(range, color);
        //             });
        //         });
        //     },
        //     provideColorPresentations(color: any, context: any): Thenable<any[]> {
        //         let params = {
        //             textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(context.document),
        //             color,
        //             range: client.code2ProtocolConverter.asRange(context.range)
        //         };
        //         return client.sendRequest((<any>vsproto).ColorPresentationRequest.type, params).then((presentations: any) => {
        //             return presentations.map((p: any) => {
        //                 let presentation = new vscode.ColorPresentation(p.label);
        //                 presentation.textEdit = p.textEdit && client.protocol2CodeConverter.asTextEdit(p.textEdit);
        //                 presentation.additionalTextEdits = p.additionalTextEdits && client.protocol2CodeConverter.asTextEdits(p.additionalTextEdits);
        //                 return presentation;
        //             });
        //         });
        //     }
        // });
        // toDispose.push(disposable);

        let tagRequestor = (document: any, position: Position) => {
            let param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position);
            return client.sendRequest(TagCloseRequest.type, param);
        };
        var disposable = activateTagClosing(tagRequestor, { html: true, handlebars: true, razor: true }, 'html.autoClosingTags', vscode);
        toDispose.push(disposable);
    });

    vscode.languages.setLanguageConfiguration('html', {
        indentationRules: {
            increaseIndentPattern: /<(?!\?|(?:area|base|br|col|frame|hr|html|img|input|link|meta|param)\b|[^>]*\/>)([-_\.A-Za-z0-9]+)(?=\s|>)\b[^>]*>(?!.*<\/\1>)|<!--(?!.*-->)|\{[^}"']*$/,
            decreaseIndentPattern: /^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/
        },
        wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
        onEnterRules: [
            {
                beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
                afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i,
                action: { indentAction: vscode.IndentAction.IndentOutdent }
            },
            {
                beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
                action: { indentAction: vscode.IndentAction.Indent }
            }
        ],
    });

    vscode.languages.setLanguageConfiguration('handlebars', {
        wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
        onEnterRules: [
            {
                beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
                afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i,
                action: { indentAction: vscode.IndentAction.IndentOutdent }
            },
            {
                beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
                action: { indentAction: vscode.IndentAction.Indent }
            }
        ],
    });

    vscode.languages.setLanguageConfiguration('razor', {
        wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
        onEnterRules: [
            {
                beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
                afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i,
                action: { indentAction: vscode.IndentAction.IndentOutdent }
            },
            {
                beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
                action: { indentAction: vscode.IndentAction.Indent }
            }
        ],
    });

    const regionCompletionRegExpr = /^(\s*)(<(!(-(-\s*(#\w*)?)?)?)?)?$/;
    vscode.languages.registerCompletionItemProvider(documentSelector, {
        provideCompletionItems(doc: any, pos: any) {
            let lineUntilPos = doc.getText(new vscode.Range(new vscode.Position(pos.line, 0), pos));
            let match = lineUntilPos.match(regionCompletionRegExpr);
            if (match) {
                let range = new vscode.Range(new vscode.Position(pos.line, match[1].length), pos);
                let beginProposal = new vscode.CompletionItem('#region', vscode.CompletionItemKind.Snippet);
                beginProposal.range = range;
                beginProposal.insertText = new vscode.SnippetString('<!-- #region $1-->');
                beginProposal.documentation = 'Folding Region Start';
                beginProposal.filterText = match[2];
                beginProposal.sortText = 'za';
                let endProposal = new vscode.CompletionItem('#endregion', vscode.CompletionItemKind.Snippet);
                endProposal.range = range;
                endProposal.insertText = new vscode.SnippetString('<!-- #endregion -->');
                endProposal.documentation = 'Folding Region End';
                endProposal.filterText = match[2];
                endProposal.sortText = 'zb';
                return [beginProposal, endProposal];
            }
            return null;
        }
    });
    window.addEventListener("html_request_" + CompletionRequest.type.method, (ev: any) => {
        var textDocumentPosition = ev.detail.params[0];
        runSafe(async () => {
            let document = documents.get(textDocumentPosition.textDocument.uri);
            let mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
            if (mode && mode.doComplete) {
                let doComplete = mode.doComplete;
                let settings = await getDocumentSettings(document, () => doComplete.length > 2);
                return doComplete(document, textDocumentPosition.position, settings);
            }
            return { isIncomplete: true, items: [] };
        }, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, ev.detail);
    });

    window.addEventListener("html_request_" + CompletionResolveRequest.type.method, (ev: any) => {
        var item = ev.detail.params[0];
        runSafe(() => {
            let data = item.data;
            if (data && data.languageId && data.uri) {
                let mode = languageModes.getMode(data.languageId);
                let document = documents.get(data.uri);
                if (mode && mode.doResolve && document) {
                    return mode.doResolve(document, item);
                }
            }
            return item;
        }, null, `Error while resolving completion proposal`, ev.detail);
    });

    window.addEventListener("html_request_" + HoverRequest.type.method, (ev: any) => {
        var textDocumentPosition = ev.detail.params[0];
        runSafe(() => {
            let document = documents.get(textDocumentPosition.textDocument.uri);
            let mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
            if (mode && mode.doHover) {
                return mode.doHover(document, textDocumentPosition.position);
            }
            return null;
        }, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`, ev.detail);
    });

    window.addEventListener("html_request_" + DocumentHighlightRequest.type.method, (ev: any) => {
        var documentHighlightParams = ev.detail.params[0];
        runSafe(() => {
            let document = documents.get(documentHighlightParams.textDocument.uri);
            let mode = languageModes.getModeAtPosition(document, documentHighlightParams.position);
            if (mode && mode.findDocumentHighlight) {
                return mode.findDocumentHighlight(document, documentHighlightParams.position);
            }
            return [];
        }, [], `Error while computing document highlights for ${documentHighlightParams.textDocument.uri}`, ev.detail);
    });

    window.addEventListener("html_request_" + DefinitionRequest.type.method, (ev: any) => {
        var definitionParams = ev.detail.params[0];
        runSafe(() => {
            let document = documents.get(definitionParams.textDocument.uri);
            let mode = languageModes.getModeAtPosition(document, definitionParams.position);
            if (mode && mode.findDefinition) {
                return mode.findDefinition(document, definitionParams.position);
            }
            return [];
        }, null, `Error while computing definitions for ${definitionParams.textDocument.uri}`, ev.detail);
    });

    window.addEventListener("html_request_" + ReferencesRequest.type.method, (ev: any) => {
        var referenceParams = ev.detail.params[0];
        runSafe(() => {
            let document = documents.get(referenceParams.textDocument.uri);
            let mode = languageModes.getModeAtPosition(document, referenceParams.position);
            if (mode && mode.findReferences) {
                return mode.findReferences(document, referenceParams.position);
            }
            return [];
        }, [], `Error while computing references for ${referenceParams.textDocument.uri}`, ev.detail);
    });

    window.addEventListener("html_request_" + SignatureHelpRequest.type.method, (ev: any) => {
        var signatureHelpParms = ev.detail.params[0];
        runSafe(() => {
            let document = documents.get(signatureHelpParms.textDocument.uri);
            let mode = languageModes.getModeAtPosition(document, signatureHelpParms.position);
            if (mode && mode.doSignatureHelp) {
                return mode.doSignatureHelp(document, signatureHelpParms.position);
            }
            return null;
        }, null, `Error while computing signature help for ${signatureHelpParms.textDocument.uri}`, ev.detail);
    });

    window.addEventListener("html_request_" + DocumentRangeFormattingRequest.type.method, async (ev: any) => {
        var formatParams = ev.detail.params[0];
        runSafe(async () => {
            let document = documents.get(formatParams.textDocument.uri);
            let settings = await getDocumentSettings(document, () => true);
            if (!settings) {
                settings = globalSettings;
            }
            let unformattedTags: string = settings && settings.html && settings.html.format && settings.html.format.unformatted || '';
            let enabledModes = { css: !unformattedTags.match(/\bstyle\b/), javascript: !unformattedTags.match(/\bscript\b/) };

            return format(languageModes, document, formatParams.range, formatParams.options, settings, enabledModes);
        }, [], `Error while formatting range for ${formatParams.textDocument.uri}`, ev.detail);
    });

    window.addEventListener("html_request_" + DocumentLinkRequest.type.method, (ev: any) => {
        var documentLinkParam = ev.detail.params[0];
        runSafe(() => {
            let document = documents.get(documentLinkParam.textDocument.uri);
            let links: any[] = [];
            if (document) {
                let documentContext = getDocumentContext(document.uri, workspaceFolders);
                languageModes.getAllModesInDocument(document).forEach((m: any) => {
                    if (m.findDocumentLinks) {
                        pushAll(links, m.findDocumentLinks(document, documentContext));
                    }
                });
            }
            return links;
        }, new Array<any>(), `Error while document links for ${documentLinkParam.textDocument.uri}`, ev.detail);
    });

    window.addEventListener("html_request_" + DocumentSymbolRequest.type.method, (ev: any) => {
        var documentSymbolParms = ev.detail.params[0];
        return runSafe(() => {
            let document = documents.get(documentSymbolParms.textDocument.uri);
            let symbols: any[] = [];
            languageModes.getAllModesInDocument(document).forEach((m: any) => {
                if (m.findDocumentSymbols) {
                    pushAll(symbols, m.findDocumentSymbols(document));
                }
            });
            return symbols;
        }, new Array<any>(), `Error while computing document symbols for ${documentSymbolParms.textDocument.uri}`, ev.detail);
    });

    window.addEventListener("html_request_" + (<any>vsproto).DocumentColorRequest.type.method, (ev: any) => {
        var params = ev.detail.params[0];
        runSafe(() => {
            let infos: any[] = [];
            let document = documents.get(params.textDocument.uri);
            if (document) {
                languageModes.getAllModesInDocument(document).forEach((m: any) => {
                    if (m.findDocumentColors) {
                        pushAll(infos, m.findDocumentColors(document));
                    }
                });
            }
            return infos;
        }, new Array<any>(), `Error while computing document colors for ${params.textDocument.uri}`, ev.detail);
    });

    window.addEventListener("html_request_" + (<any>vsproto).ColorPresentationRequest.type.method, (ev: any) => {
        var params = ev.detail.params[0];
        runSafe(() => {
            let document = documents.get(params.textDocument.uri);
            if (document) {
                let mode = languageModes.getModeAtPosition(document, params.range.start);
                if (mode && mode.getColorPresentations) {
                    return mode.getColorPresentations(document, params.color, params.range);
                }
            }
            return [];
        }, [], `Error while computing color presentations for ${params.textDocument.uri}`, ev.detail);
    });

    window.addEventListener("html_request_" + TagCloseRequest.type.method, (ev: any) => {
        var params = ev.detail.params[0];
        runSafe(() => {
            let document = documents.get(params.textDocument.uri);
            if (document) {
                let pos = params.position;
                if (pos.character > 0) {
                    let mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
                    if (mode && mode.doAutoClose) {
                        return mode.doAutoClose(document, pos);
                    }
                }
            }
            return null;
        }, null, `Error while computing tag close actions for ${params.textDocument.uri}`, ev.detail);
    });
}

